﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using Spring.Context;
using Spring.Context.Support;
using System.Threading;
using System.IO;
using Spring.Objects.Factory;
using Flower.Logging.Sinks;

namespace Flower.Logging
{
    public class LogManager
    {
        private enum LogCommandType
        {
            Dispatch,
            Flush,
            Stop
        }

        private struct LogCommand
        {
            public LogCommandType Type;
            public LogEntry Entry;
            public ManualResetEventSlim Processed;
        }

        private const string CONTAINER_CONFIG_FILE_SETTING = "Flower.Logging.ContainerConfigurationFile";
        private const string LOG_MANAGER_ID_SETTING = "Flower.Logging.LogManagerId";

        private const string DEFAULT_LOG_FILE_NAME = "Default.log";
        private const string DEFAULT_CONTAINER_CONFIG_FILE = "Flower.Logging.config";
        private const string DEFAULT_LOG_MANAGER_ID = "logManager";

        internal static readonly IApplicationContext AppContext;
        internal static readonly LogManager Instance;

        private readonly LoggingLevel _loggingLevel;
        private readonly ILogSink[] _logSinks;
        private readonly BlockingCollection<LogCommand>[] _logChannels;
        private readonly ManualResetEventSlim[] _threadStopEvents;
        private readonly Timer _flushTimer;

        private bool _isStopped;

        static LogManager()
        {
            var defaultSettings =
                new LogManagerSettings
                {
                    LoggingLevel = LoggingLevel.Info,
                    FlushPeriod = TimeSpan.FromSeconds(1),
                    Sinks = new List<ILogSink>
                    {
                        new FileLogSink(DEFAULT_LOG_FILE_NAME)
                    }
                };

            try
            {
                string containerConfigFile = 
                    string.Concat
                    (
                        AppDomain.CurrentDomain.BaseDirectory, 
                        @"\",
                        ConfigurationManager.AppSettings[CONTAINER_CONFIG_FILE_SETTING] ?? DEFAULT_CONTAINER_CONFIG_FILE
                    );

                string logManagerId =
                    ConfigurationManager.AppSettings[LOG_MANAGER_ID_SETTING] ?? DEFAULT_LOG_MANAGER_ID;

                AppContext = new XmlApplicationContext(containerConfigFile);

                Instance = (LogManager)AppContext.GetObject(logManagerId);

                AppDomain.CurrentDomain.DomainUnload += 
                    (sender, args) => Instance.Stop();
                AppDomain.CurrentDomain.ProcessExit += 
                    (sender, args) => Instance.Stop();
            }
            catch (ObjectDefinitionStoreException ex)
            {
                var fileNotFoundException = ex.InnerException as FileNotFoundException;
                if (fileNotFoundException != null)
                {
                    Console.Error.WriteLine("Flower.Logging: Logging configuration file {0} not found.", fileNotFoundException.FileName);
                }
                else
                {
                    Console.Error.WriteLine("Flower.Logging: Failed to initialize logging.\n" + ex);
                }
                
                Instance = new LogManager(defaultSettings);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine("Flower.Logging: Failed to initialize logging.\n" + ex);
                Instance = new LogManager(defaultSettings);
            }
        }

        internal LogManager(LogManagerSettings settings)
        {
            if (settings == null)
            {
                return;
            }

            _loggingLevel = settings.LoggingLevel;

            if (settings.FlushPeriod > TimeSpan.Zero)
            {
                _flushTimer = new Timer(state => Flush(false), null, settings.FlushPeriod, settings.FlushPeriod);
            }

            if (settings.Sinks != null)
            {
                _logSinks = settings.Sinks.ToArray();
            }
            else
            {
                _logSinks = new ILogSink[0];
            }

            _logChannels = new BlockingCollection<LogCommand>[_logSinks.Length];
            _threadStopEvents = new ManualResetEventSlim[_logSinks.Length];

            for (int i = 0; i < _logSinks.Length; i++)
            {
                _logChannels[i] = new BlockingCollection<LogCommand>();
                _threadStopEvents[i] = new ManualResetEventSlim();

                ThreadPool.QueueUserWorkItem(Dispatcher, i);
            }
        }

        private void Flush(bool wait)
        {
            var events = (wait) ? new ManualResetEventSlim[_logChannels.Length] : null;
            int i = 0;

            foreach (BlockingCollection<LogCommand> ch in _logChannels)
            {
                if (wait)
                {
                    var ev = new ManualResetEventSlim(false);
                    ch.Add(new LogCommand { Type = LogCommandType.Flush, Processed = ev });
                    events[i++] = ev;
                }
                else
                {
                    ch.Add(new LogCommand { Type = LogCommandType.Flush });
                }
            }

            if (wait)
            {
                foreach (ManualResetEventSlim ev in events)
                {
                    ev.Wait();
                }
            }
        }

        private void Dispatcher(object arg)
        {
            var i = (int)arg;

            bool run = true;

            while (run)
            {
                try
                {
                    var command = _logChannels[i].Take();

                    switch (command.Type)
                    {
                        case LogCommandType.Dispatch:
                            _logSinks[i].Dispatch(command.Entry);
                            break;
                        case LogCommandType.Flush:
                            _logSinks[i].Flush();
                            break;
                        case LogCommandType.Stop:
                            _threadStopEvents[i].Set();
                            run = false;
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }

                    if (command.Processed != null)
                    {
                        command.Processed.Set();
                    }
                }
                catch (ThreadAbortException) { return; }
                catch (Exception)
                {
                     /* Do nothing */
                }
            }
        }

        internal void Stop()
        {
            if (_isStopped)
            {
                return;
            }

            if (_flushTimer != null)
            {
                _flushTimer.Dispose();
            }

            foreach (BlockingCollection<LogCommand> ch in _logChannels)
            {
                ch.Add(new LogCommand { Type = LogCommandType.Flush });
                ch.Add(new LogCommand { Type = LogCommandType.Stop });
            }

            foreach (ManualResetEventSlim ev in _threadStopEvents)
            {
                ev.Wait(TimeSpan.FromSeconds(30));
            }

            _isStopped = true;
        }

        internal void Dispatch(LogEntry entry)
        {
            if (entry.Severity <= _loggingLevel)
            {
                if (entry.MessageFunc != null)
                {
                    try
                    {
                        entry.Message = entry.MessageFunc();
                    }
                    catch (Exception ex)
                    {
                        entry.Message = "Error evaluating message.";
                        entry.Severity = LoggingLevel.Error;
                        entry.Exception = ex;
                    }

                    entry.MessageFunc = null;
                }

                entry.Timestamp = DateTime.Now;

                foreach (BlockingCollection<LogCommand> ch in _logChannels)
                {
                    ch.Add(new LogCommand { Type = LogCommandType.Dispatch, Entry = entry });
                }
            }
        }

        public static Log CreateLog(string source)
        {
            return new Log(source);
        }

        public static void Flush()
        {
            Instance.Flush(true);
        }
    }

    internal class LogManagerSettings
    {
        public LoggingLevel LoggingLevel { get; set; }
        public ICollection<ILogSink> Sinks { get; set; }
        public TimeSpan FlushPeriod { get; set; }
    }
}
